;(function(){

var myName = 'CombinatorPage',
	Me = self[myName] = MVC.create(myName)

// Me.mixIn(EventDriven)

var myProto =
{
	initialize: function ()
	{
		this.model.initialize()
		this.view.initialize()
		this.controller.initialize()
	},

	bind: function (nodes)
	{
		this.view.bind(nodes)
		this.model.bind()
		
		this.view.locationHashUpdated()
		
		return this
	}
}

Object.extend(Me.prototype, myProto)

})();

if (!('onhashchange' in window) && !/Opera\/.+ Version\/1\d\.[6789]/.test(navigator.userAgent))
(function(){

// mark browser as supporting one
window.onhashchange = null

var location = window.location

var lastHash = location.hash
function check ()
{
	var hash = location.hash
	
	if (hash === lastHash)
		return
	lastHash = hash
	
	var e = document.createEvent('Event')
	e.initEvent('hashchange', true, false)
	document.dispatchEvent(e)
}

var timer
function restart (d)
{
	clearInterval(timer)
	if (d < 0)
		return
	
	timer = setInterval(check, d || 500)
}

window.location.setHashchangeCheckInterval = restart

restart()
document.addEventListener('mouseup', function () { setTimeout(function () { check() }, 0) }, true)

})();

if (/WebKit\/|MSIE [6789]\./.test(navigator.userAgent))
(function(){

var status = {}

document.addEventListener
(
	'keydown',
	function (e)
	{
		var target = e.target
		
		var ne = document.createEvent('Event')
		ne.initEvent('keypress', true, true)
		
		// copying valueable data
		ne.altKey = e.altKey
		ne.ctrlKey = e.ctrlKey
		ne.metaKey = e.metaKey
		ne.charCode = e.charCode
		ne.keyCode = e.keyCode
		
		if (status[e.keyCode])
			e.stopPropagation()
		else
			status[e.keyCode] = true
		
		if (!target.dispatchEvent(ne))
			e.preventDefault()
	},
	true
)

document.addEventListener
(
	'keyup',
	function (e)
	{
		status[e.keyCode] = false
	},
	true
)

})();

;(function(){

var myProto =
{
	childIndexedPath: function (node)
	{
		var path = []
		path:
		for (;;)
		{
			if (node == this)
				break
			
			var parent = node.parentNode
			if (!parent)
				return null
			
			var childs = parent.childNodes
			for (var i = 0, num = 0, il = childs.length; i < il; i++)
			{
				var child = childs[i]
				if (child == node)
				{
					path.push(num)
					node = parent
					continue path
				}
				// to provide a path useful cross browser lets count only the elements
				else if (child.nodeType == 1)
					num++
			}
			// we'r here if child node lied to us about its parentNode
			return null
		}
		
		return path.reverse()
	},
	
	getChildByIndexedPath: function (path)
	{
		var node = this
		path:
		for (var i = 0, il = path.length; node && i < il; i++)
		{
			var n = path[i],
				childs = node.childNodes
			// if there is no children this loop will be skipped
			// so we can not go too deep
			for (var j = 0, jl = childs.length; j < jl; j++)
			{
				var child = childs[j]
				if (child.nodeType == 1)
					if (n == 0)
					{
						node = child
						continue path
					}
					else
						n--
			}
			// we are here only if child num goes out of bounds
			return null
		}
		return node
	}
}

Object.add(Element.prototype, myProto)
Object.add(document, myProto)


})();
;(function(){

var myName = 'Cloner'

function Me ()
{
	this.nodes = {}
	this.constructor = Me
}

Me.prototype =
{
	bind: function (root, nodes)
	{
		this.root = root
		this.nodes = nodes
		
		this.sync()
		
		return this
	},
	
	sync: function ()
	{
		this.paths = this.getPaths(this.root, this.nodes)
	},
	
	getPaths: function (root, nodes)
	{
		var paths = {}
		for (var k in nodes)
			paths[k] = root.childIndexedPath(nodes[k])
		return paths
	},
	
	getNodes: function (root, paths)
	{
		var nodes = {}
		for (var k in paths)
			nodes[k] = root.getChildByIndexedPath(paths[k])
		return nodes
	},
	
	create: function ()
	{
		var root = this.root.cloneNode(true),
			nodes = this.getNodes(root, this.paths)
		
		return {root: root, nodes: nodes}
	}
}

// Me.mixIn(EventDriven)
Me.className = myName
self[myName] = Me

})();
;(function(){

var rex = /([\\\.\*\+\?\$\^\|\(\)\[\]\{\}])/g

if (!RegExp.escape)
RegExp.escape = function (str)
{
	return ('' + str).replace(rex, '\\$1')
}

})();

;(function(){

var Me =
{
	decode: decodeURIComponent,
	encode: encodeURIComponent,
	paramDelimiter: '&',
	
	parse: function (string, forceArray)
	{
		var decode = this.decode
		var res = {}
		
		var parts = String(string).split(this.paramDelimiter)
		for (var i = 0; i < parts.length; i++)
		{
			var pair = parts[i].split('='),
				k = pair[0],
				v = pair[1]
			
			if (v === undefined)
			{
				if (k == '')
					continue
			}
			else
				v = decode(v)
			
			k = decode(k)
			
			if (forceArray)
			{
				if (res[k])
					res[k].push(v)
				else
					res[k] = [v]
			}
			else
			{
				var a = res[k]
				if (a)
				{
					if (typeof res[k] == 'object')
						res[k].push(v)
					else
						res[k] = [res[k], v]
				}
				else
					res[k] = v
			}
		}
		
		return res
	},
	
	stringify: function (data)
	{
		var encode = this.encode, A = Array,
			pairs = []
		
		for (var k in data)
		{
			var v = data[k]
			
			k = encode(k)
			if (v && v.constructor == A)
			{
				for (var i = 0, il = v.length; i < il; i++)
					pairs.push(k + '=' + encode(v[i]))
			}
			else
				pairs.push(k + '=' + encode(v))
		}
		
		return pairs.join(this.paramDelimiter)
	}
}

Me.className = 'UrlEncode'
self[Me.className] = Me

})();
;(function(){

var myName = 'LocationHash'

function Me () {}

function dontTouch (v) { return v }

Me.prototype =
{
	encode: encodeURI,
	decode: decodeURIComponent,
	
	bind: function (win)
	{
		if (!win)
			win = window
		
		this.window = win
		
		function decodesOnTheFly ()
		{
			var a = document.createElement('a')
			a.href = 'abc'
			a.hash = encodeURIComponent('%26')
			return a.hash === '#%26'
		}
		
		if (decodesOnTheFly())
			Me.prototype.decode = dontTouch
		
		this.value = this.get()
		
		var me = this
		function onhashchange (e) { me.onhashchange() }
		win.addEventListener('hashchange', onhashchange, false)
		
		return this
	},
	
	onhashchange: function (e)
	{
		var v = this.get()
		if (v === this.value)
			return
		this.value = v
		
		this.dispatchEventData('change')
	},
	
	set: function (v)
	{
		this.value = v
		this.window.location.hash = '#' + this.encode(v)
	},
	
	get: function ()
	{
		var v = this.window.location.hash.substr(1)
		
		try
		{
			return this.decode(v)
		}
		catch (ex)
		{
			return v
		}
	}
}

Me.mixIn(EventDriven)

Me.className = myName
self[myName] = Me

})();


;(function(){

var myName = 'Selecter'

function Me (group)
{
	this.nodes = {optionsCache: []}
	this.constructor = Me
	
	this.valToNum = {}
	this.options = []
	this.optionPresent = []
	
	this.lastSelected = -1
	
	this.group = group
	
	var arr = Me.groups[group]
	if (arr)
		arr.push(this)
	else
		Me.groups[group] = [this]
}

Me.groups = {}
Me.closeGoup = function (group)
{
	var arr = Me.groups[group]
	
	if (!arr)
		return
	
	for (var i = 0, il = arr.length; i < il; i++)
		arr[i].close()
}


eval(NodesShortcut.include())

Me.prototype =
{
	bind: function (nodes)
	{
		this.nodes = nodes
		nodes.optionsCache = []
		
		this.bindEvents()
		
		return this
	},
	
	bindEvents: function ()
	{
		var nodes = this.nodes,
			main = nodes.main,
			me = this
		
		function open (e)
		{
			e.preventDefault()
			Me.closeGoup(me.group)
			
			main.addClassName('open')
			main.addEventListener('mousedown', close, false)
			main.removeEventListener('mousedown', open, false)
			document.addEventListener('mouseup', close, false)
		}
		
		function close ()
		{
			main.removeClassName('open')
			main.addEventListener('mousedown', open, false)
			main.removeEventListener('mousedown', close, false)
			document.removeEventListener('mouseup', close, false)
		}
		
		function select (e)
		{
			e.stopPropagation()
			close(e)
			me.select(e.target.getAttribute('data-selecter-option-num'))
		}
		
		this.close = close
		
		main.addEventListener('mousedown', open, false)
		main.addEventListener('mouseup', function (e) { e.stopPropagation() }, false)
		
		var options = nodes.options
		options.addEventListener('mousedown', select, false)
		options.addEventListener('mouseup', select, false)
	},
	
	close: function () {},
	
	setOptions: function (options)
	{
		var nodes = this.nodes,
			root = nodes.options,
			main = nodes.main
		
		root.empty()
		var valToNum = this.valToNum = {},
			optionsCache = nodes.optionsCache = []
		
		this.options = options
		var optionPresent = this.optionPresent = []
		
		if (!options.length)
		{
			main.removeClassName('single')
			main.addClassName('empty')
			return options
		}
		
		main.removeClassName('empty')
		
		for (var i = 0; i < options.length; i++)
		{
			var option = options[i]
			
			valToNum[option] = i
			optionPresent[i] = true
			
			var li = optionsCache[i] = Nc('li', 'option')
			li.setAttribute('data-selecter-option-num', i)
			li.appendChild(T(option))
			root.appendChild(li)
		}
		
		if (options.length == 1)
			main.addClassName('single')
		else
			main.removeClassName('single')
		
		return options
	},
	
	setCaption: function (value)
	{
		var button = this.nodes.button
		button.empty()
		button.appendChild(T(value))
	},
	
	select: function (num)
	{
		if (!this.optionPresent[num])
			return
		
		if (!this.dispatchEventData('select', {value: this.options[num], num: num}))
			return
		
		this.renderSelected(num)
		
	},
	
	renderSelectedValue: function (value)
	{
		this.renderSelected(this.valToNum[value])
	},
	
	renderSelected: function (num)
	{
		if (num === this.lastSelected)
			return
		
		var optionsCache = this.nodes.optionsCache
			
		var last = optionsCache[this.lastSelected]
		if (last)
			last.removeClassName('selected')
		
		optionsCache[num].addClassName('selected')
		
		this.setCaption(this.options[num])
		
		var main = this.nodes.main
		main.removeClassName('selected-option-' + this.lastSelected)
		main.addClassName('selected-option-' + num)
		
		this.lastSelected = num
	}
}

Me.mixIn(EventDriven)
Me.className = myName
self[myName] = Me

})();

var Selecter1 =
{
	stack: [],
	
	bind: function (main)
	{
		main.nodes =
		{
			caption: main.getElementsByClassName('caption')[0],
			options: main.getElementsByClassName('options')[0]
		}
		
		if (!main.onselect)
		 	main.onselect = function () {}
		
		main.setCaption = function (str)
		{
			this.nodes.caption.innerHTML = str
		}
		
		main.setOptions = function (options)
		{
			var optionsNode = this.nodes.options
			optionsNode.empty()
			
			this.remClassName('empty')
			this.remClassName('single')
			
			if (options && options.length)
			{
				for (var i = 0; i < options.length; i++)
				{
					var li = document.createElement('li')
					li.appendChild(document.createTextNode(options[i]))
					optionsNode.appendChild(li)
				}
				
				if (options.length == 1)
					this.addClassName('single')
			}
			else
				this.addClassName('empty')
			
			return options
		}
		
		main.getNodeNumber = function (node)
		{
			var optionsChilds = this.nodes.options.childNodes
			for (var i = 0; i < optionsChilds.length; i++)
				if (optionsChilds[i] == node)
					return i
			return -1
		}
		
		main.getValueNumber = function (val)
		{
			var optionsChilds = this.nodes.options.childNodes
			for (var i = 0; i < optionsChilds.length; i++)
				if (optionsChilds[i].innerHTML == val)
					return i
			return -1
		}
		
		main.removeGarbageNodes = function ()
		{
			var options = this.nodes.options
			var optionsChilds = this.nodes.options.childNodes
			for (var i = 0; i < optionsChilds.length; i++)
				if (optionsChilds[i].nodeType != 4)
					options.removeChild(optionsChilds[i])
		}
		
		main.select = function (num, force)
		{
			if (typeof num !== 'number')
				num = this.getValueNumber(num)
			var optionsChilds = Array.copy(this.nodes.options.childNodes)
			var selected = optionsChilds[num]
			if (selected)
			{
				if (!force && this.onselect(selected.innerHTML, num, selected) === false)
					return
				
				for (var i = 0; i < optionsChilds.length; i++)
					optionsChilds[i].remClassName('selected')
				
				selected.addClassName('selected')
				this.setCaption(selected.innerHTML)
			}
			else
				log('Can`t select option with value "'+num+'": no such option`')
		}
		
		main.removeGarbageNodes()
		
		function close (e)
		{
			main.remClassName('open')
			main.addEventListener('mousedown', open, false)
			main.removeEventListener('mousedown', close, false)
			document.removeEventListener('mouseup', close, false)
		}
		
		function open (e)
		{
			if (main.nodes.options.childNodes.length < 2)
				return
			Selecter.closeAll()
			e.stopPropagation()
			main.addClassName('open')
			main.addEventListener('mousedown', close, false)
			main.removeEventListener('mousedown', open, false)
			document.addEventListener('mouseup', close, false)
		}
		
		function select (e)
		{
			e.stopPropagation()
			close()
			var target = e.target
			if (target.nodeName == 'LI')
				main.select(main.getNodeNumber(target))
		}
		
		main.open = open
		main.close = close
		
		main.addEventListener('mousedown', open, false)
		main.addEventListener('mouseup', function (e) { e.stopPropagation() }, false)
		main.nodes.options.addEventListener('mousedown', select, false)
		main.nodes.options.addEventListener('mouseup', select, false)
		
		main.setOptions([])
		this.stack.push(main)
		
		return main
	},
	
	closeAll: function ()
	{
		for (var i = 0; i < this.stack.length; i++)
			this.stack[i].close()
	}
}


;(function(){

var Papa

;(function(){

var myName = 'Autocompleter',
	Me = Papa = self[myName] = MVC.create(myName)

var myProto =
{
	initialize: function ()
	{
		this.model.initialize()
		this.view.initialize()
		this.controller.initialize()
	},
	
	bind: function (nodes, count)
	{
		this.view.bind(nodes)
		this.setCount(count || 10)
		return this
	},
	
	reset: function () { this.controller.reset() },
	search: function (v) { this.controller.search(v) },
	setDataSource: function (ds) { this.model.dataSource = ds },
	setCount: function (v) { this.model.setCount(v) }
}

Me.mixIn(EventDriven)

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.View

eval(NodesShortcut.include())

var myProto =
{
	initialize: function ()
	{
		this.nodes = {}
		this.listeners = {}
	},
	
	bind: function (nodes)
	{
		this.nodes = nodes
		
		var me = this
		this.listeners.keypress = function (e) { return me.onKeyPress(e) }
	},
	
	keyMap: {38: 'prev', 40: 'next', 27: 'reset', 13: 'accept'},
	onKeyPress: function (e)
	{
		// log(e.keyCode)
		var action = this.keyMap[e.keyCode]
		
		if (!action)
			return
		
		e.preventDefault()
		e.stopPropagation()
		
		var controller = this.controller
		setTimeout(function () { controller.action(action) }, 0)
	},
	
	focus: function ()
	{
		if (this.focused)
			return
		this.focused = true
		
		this.nodes.list.addClassName('focused')
		document.addEventListener('keypress', this.listeners.keypress, true)
	},
	
	blur: function ()
	{
		if (!this.focused)
			return
		this.focused = false
		
		this.nodes.list.removeClassName('focused')
		document.removeEventListener('keypress', this.listeners.keypress, true)
	},
	
	createItemsNodes: function (count)
	{
		var list = this.nodes.list, items = this.nodes.items = []
		
		var me = this
		function mousemove (e) { me.onMouseMove(this) }
		function mousedown (e) { me.onMouseDown(this) }
		
		for (var i = 0; i < count; i++)
		{
			var item = items[i] = N('li')
			item.className = 'item hidden'
			list.appendChild(item)
			item.setAttribute('data-autocompleter-num', i)
			item.addEventListener('mousemove', mousemove, false)
			item.addEventListener('mousedown', mousedown, false)
		}
	},
	
	onMouseMove: function (node)
	{
		this.controller.suggest(+node.getAttribute('data-autocompleter-num'))
	},
	
	onMouseDown: function (node)
	{
		this.controller.accept(+node.getAttribute('data-autocompleter-num'))
	},
	
	render: function (results)
	{
		if (!results)
		{
			this.blur()
			return
		}
		
		this.focus()
		this.renderItems(results)
	},
	
	renderItems: function (results)
	{
		var items = this.nodes.items
		for (var i = 0, il = items.length, rl = results.length; i < rl && i < il; i++)
		{
			var r = results[i],
				item = items[i]
			item.empty()
			item.appendChild(r[1]) // [1] means a text representing node (or DocumentFragment)
			item.removeClassName('hidden')
		}
		
		for (; i < items.length; i++)
			items[i].addClassName('hidden')
		
		this.nodes.list.toggleClassName('empty', rl == 0)
	},
	
	selectItem: function (num)
	{
		if (this.selected === num)
			return
		
		var items = this.nodes.items
		
		var node = items[this.selected]
		if (node)
			node.removeClassName('selected')
		
		var node = items[num]
		if (node)
			node.addClassName('selected')
		
		this.selected = num
	}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.Controller

var myProto =
{
	search: function (v)
	{
		this.model.search(v)
	},
	
	prev: function ()
	{
		this.model.selectBy(-1)
	},
	
	next: function ()
	{
		this.model.selectBy(1)
	},
	
	select: function (num)
	{
		this.model.select(num)
	},
	
	suggest: function (num)
	{
		this.model.suggest(num)
	},
	
	accept: function (num)
	{
		this.model.accept(num)
	},
	
	reset: function (num)
	{
		this.model.reset(num)
	},
	
	action: function (action)
	{
		this[action]()
	}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.Model

var myProto =
{
	dataSource: {search: function () { return [] }},
	
	initialize: function ()
	{
		this.results = null
		this.selected = -1
		this.value = null
	},
	
	setCount: function (count)
	{
		this.count = count
		this.view.createItemsNodes(count)
	},
	
	reset: function ()
	{
		this.initialize()
		this.view.render(null)
	},
	
	selectBy: function (dir)
	{
		var results = this.results
		if (!results)
			return
		
		var total = results.length,
			selected = this.selected
		
		selected += dir
		
		// -1 is a special case means “nothing selected”
		if (selected < -1)
			selected = total - 1
		else if (selected >= total)
			selected = -1
		
		this.select(selected)
	},
	
	select: function (num)
	{
		if (this.selected === num)
			return
		
		var results = this.results
		if (!results)
			return
		
		var v, res = results[num]
		if (res)
		{
			v = res[0]
		}
		else
		{
			v = new String(this.value)
			v.source = true
		}
			
		if (!this.parent.dispatchEvent({type: 'select', num: num, value: v}))
			return false
		
		this.selected = num
		this.view.selectItem(num)
	},
	
	suggest: function (num)
	{
		this.selected = num
		this.view.selectItem(num)
	},
	
	accept: function (num)
	{
		var results = this.results
		if (!results)
			return
		
		if (num === undefined)
			num = this.selected
		
		if (this.select(num) === false)
			return false
		
		var v, res = results[num]
		if (res)
		{
			v = res[0]
		}
		else
		{
			v = new String(this.value)
			v.source = true
		}
		
		if (!this.parent.dispatchEvent({type: 'accept', num: num, value: v}))
			return false
		
		this.reset()
	},
	
	search: function (value)
	{
		this.value = value
		var res = this.dataSource.search(value, this.count)
		this.setResults(res)
	},
	
	setResults: function (results)
	{
		this.selected = -1
		this.results = results
		this.view.render(results)
		this.view.selectItem(-1)
	}
}

Object.extend(Me.prototype, myProto)

})();


})();

;(function(){

var myName = 'AddingInputAutocompleter'

function Me ()
{
	this.nodes = {}
	this.constructor = Me
}

Me.prototype =
{
	// ignore “non-content” keycodes
	ignoreKeys: {9:1, 16:1, 17:1, 27:1, 33:1, 34:1, 35:1, 36:1, 38:1, 18:1, 91:1, 13:1},
	preventKeys: {40:1},
	actionKeys: {37:'cursor', 39:'cursor', 40:'search'},
	
	bind: function (nodes)
	{
		this.nodes = nodes
		
		var completer = this.completer = new Autocompleter()
		completer.bind(nodes)
		
		completer.addEventListener('accept', function (e) { me.accept(e.value) }, false)
		completer.addEventListener('select', function (e) { me.select(e.value) }, false)
		
		var me = this
		function onupdate (e) { me.onUpdate(e) }
		nodes.main.addEventListener('keypress', onupdate, false)
		nodes.main.addEventListener('blur', function (e) { me.onBlur() }, false)
		
		this.tokenizer = new Tokenizer(nodes.main)
		this.lastCurrentTokenNum = this.tokenizer.getCurrentNum()
		
		return this
	},
	
	search: function ()
	{
		var tokenizer = this.tokenizer
		
		var parts = tokenizer.getParts(true)
		this.dispatchEvent({type: 'changed', add: parts.add, remove: parts.remove})
		
		this.lastCurrentTokenNum = this.tokenizer.getCurrentNum()
		
		var value = tokenizer.getCurrentValue()
		if (value === '')
			return this.reset()
		
		this.completer.search(value)
	},
	
	reset: function ()
	{
		this.completer.reset()
	},
	
	onUpdate: function (e)
	{
		var keyCode = e.keyCode
		// log(keyCode)
		
		if (this.ignoreKeys[keyCode])
			return
		
		if (this.preventKeys[keyCode])
		{
			e.preventDefault()
			e.stopPropagation()
		}
		
		var action = this.actionKeys[keyCode]
		
		var me = this
		setTimeout(function () { me.action(action) }, 1)
	},
	
	onBlur: function ()
	{
		this.reset()
	},
	
	action: function (action)
	{
		if (action)
			return this[action]()
		
		this.search()
	},
	
	cursor: function ()
	{
		var num = this.tokenizer.getCurrentNum()
		
		if (num != this.lastCurrentTokenNum)
		{
			this.lastCurrentTokenNum = num
			this.reset()
		}
	},
	
	apply: function (value)
	{
		var parts = this.tokenizer.getParts()
		this.completer.reset()
		this.dispatchEvent({type: 'accept', add: parts.add, remove: parts.remove})
	},
	
	select: function (value)
	{
		this.tokenizer.setCurrentValue(value)
	},
	
	accept: function (value)
	{
		this.select(value)
		
		var parts = this.tokenizer.getParts()
		this.dispatchEvent({type: 'accept', value: value, add: parts.add, remove: parts.remove})
	},
	
	set: function (value)
	{
		var input = this.nodes.main
		input.value = value
		this.apply()
	},
	
	
	setDataSource: function (ds)
	{
		return this.completer.setDataSource(ds)
	}
}

Me.mixIn(EventDriven)
Me.className = myName
self[myName] = Me

})();
;(function(){

var myName = 'IngredientsSearcher'

function Me (ingredients, names)
{
	this.ingredients = ingredients || []
	this.names = names || {}
	this.cache = {}
	this.duplicates = {}
	this.favorites = {}
}

eval(NodesShortcut.include())

Me.prototype =
{
	search: function (substr, count)
	{
		substr = ('' + substr).trim()
		if (substr === '')
			return []
		
		var parts = substr.split(/ +/)
		
		var cache = this.cache
		var rows = cache[substr]
		if (!rows)
			rows = cache[substr] = this.searchInSet(this.ingredients, parts)
		
		var duplicates = this.duplicates, favorites = this.favorites,
			filtered = []
		
		for (var i = 0, il = rows.length; i < il; i ++)
		{
			var row = rows[i]
			if (duplicates[row[1]])
				row[0] *= 100000
			else if (favorites[row[1]])
				row[0] *= 0.000001
		}
		
		rows.sort(this.sortByWeight)
		
		var res = []
		for (var i = 0, il = rows.length; i < il && i < count; i++)
			res[i] = rows[i][1]
		
		return this.renderRows(res, parts, this.names, this.duplicates)
	},
	
	searchInSet: function (set, parts)
	{
		parts = parts.slice()
		// first search for the most lengthy part
		parts.sort(this.sortByLength)
		
		// lightweight rex for initial filtering
		var filter = new RegExp('(?:^|[ \\-])' + RegExp.escape(parts[0]), 'i')
		
		var rexes = []
		for (var i = 0, il = parts.length; i < il; i++)
			rexes[i] = new RegExp('(^|.*[ \\-])(' + RegExp.escape(parts[i]) + '[^ \\-]*)', 'i')
		
		
		var rows = [], rl = rexes.length
		set: for (var i = 0, il = set.length; i < il; i++)
		{
			var v = set[i]
			
			if (!filter.test(v))
				continue set
			
			var weight = 0
			for (var j = 0; j < rl; j++)
			{
				var m = rexes[j].exec(v)
				if (!m)
					continue set
				
				// weight += (10000 * m[2].length) + (100 * m[1].length) + v.length
				weight += (10000 * m[2].length) + m[1].length
			}
			
			rows.push([weight, v])
		}
		
		return rows
	},
	
	renderRows: function (values, parts, names, duplicates)
	{
		var res = []
		
		var rexes = []
		for (var i = 0, il = parts.length; i < il; i++)
		{
			var p = parts[i]
			var rex = rexes[i] = new RegExp('(?:^|.*[ \\-])(' + RegExp.escape(p) + '[^ \\-]*)', 'gi')
			rex.part = p
		}
		
		for (var i = 0, il = values.length; i < il; i++)
		{
			var v = values[i]
			
			
			// find all sun-matches
			var matches = []
			for (var j = 0, jl = rexes.length; j < jl; j++)
			{
				var rex = rexes[j]
				
				rex.lastIndex = 0
				var m = rex.exec(v)
				if (!m)
					continue
				
				var begin = rex.lastIndex - m[1].length
				matches.push([begin, begin + rex.part.length])
			}
			
			
			// sort sub-matches so as the nearest left and biggest will be first
			function byPositionAndLength (a, b)
			{
				return a[0] - b[0] || (b[1] - b[0]) - (a[1] - a[0])
			}
			
			matches.sort(byPositionAndLength)
			
			
			// filt-out overlays
			var last = 0, filtered = []
			for (var j = 0, jl = matches.length; j < jl; j++)
			{
				var m = matches[j]
				if (m[0] < last)
					continue
				
				filtered.push(m)
				last = m[1]
			}
			matches = filtered
			
			
			// split value into row of nodes
			var text = N('span')
			
			var pos = 0
			for (var j = 0, jl = matches.length; j < jl; j++)
			{
				var m = matches[j],
					begin = m[0],
					end = m[1]
				
				if (pos != begin)
					text.appendChild(T(v.substring(pos, begin)))
				
				text.appendChild(Nct('span', 'substr', v.substring(begin, end)))
				
				pos = end
			}
			
			if (pos < v.length)
				text.appendChild(T(v.substring(pos)))
			
			var value = v
			
			var name = names[v]
			if (name)
			{
				text.appendChild(T(' — это «' + name + '»'))
				value = name
			}
			
			var duplicate = duplicates[v]
			if (duplicate)
			{
				if (duplicate === true)
					text.appendChild(Nct('span', 'hint', ' (уже выбрано)'))
				else
					text.appendChild(Nct('span', 'hint', ' (уже в группе «' + duplicate + '»)'))
				
				text.className += ' duplicate'
			}
			
			res[i] = [value, text]
		}
		
		return res
	},
	
	sortByWeight: function (a, b) { return a[0] - b[0] },
	sortByLength: function (a, b) { return b.length - a.length }
}

Me.className = myName
self[Me.className] = Me

})();

;(function(){

var Papa

;(function(){

var myName = 'IngredientedCocktailList',
	Me = Papa = self[myName] = MVC.create(myName)

var myProto =
{
	initialize: function ()
	{
		this.model.initialize()
		this.view.initialize()
		this.controller.initialize()
	},
	
	bind: function (nodes)
	{
		this.view.bind(nodes)
		
		return this
	},
	
	setCocktails: function (cocktails)
	{
		this.model.setCocktails(cocktails)
	},
	
	wake: function ()
	{
		this.view.wake()
	},
	
	sleep: function ()
	{
		this.view.sleep()
	}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.View

eval(NodesShortcut.include())

function joinWithNodeToFragment (arr, node)
{
	var len = arr.length,
		res = document.createDocumentFragment()
	
	if (!len)
		return res
	
	for (var i = 0, il = len - 1; i < il; i++)
	{
		res.appendChild(arr[i])
		res.appendChild(node.cloneNode(true))
	}
	res.appendChild(arr[i])
	
	return res
}

var months = ['январь','февраль','март','апрель','май','июнь','июль','август','сентябрь','октябрь','ноябрь','декабрь']

var myProto =
{
	initialize: function ()
	{
		this.nodes = {}
		this.cache = {cocktails:{}}
	},
	
	bind: function (nodes)
	{
		this.nodes = nodes
		
		var me = this
		
		this.onclickGroupName = function () { me.groupNameClicked(this['data-group-num']) }
		
		var t = new Throttler(function () { me.onscroll() }, 100, 500)
		this.onscrollListener = function () { t.call() }
		
		this.wake()
	},
	
	wake: function ()
	{
		window.addEventListener('scroll', this.onscrollListener, false)
	},
	
	sleep: function ()
	{
		window.removeEventListener('scroll', this.onscrollListener, false)
	},
	
	onscroll: function ()
	{
		var frame = this.frame
		if (frame)
			frame.moveTo(window.pageXOffset, window.pageYOffset - 2500)
	},
	
	setupVisibilityFrame: function (nodes)
	{
		if (!nodes.length)
			return
		
		var boxes = Boxer.nodesToBoxes(nodes)
		
		var frame = this.frame = new VisibilityFrame()
		frame.setFrame(4000, 5000) // hardcoded for now
		frame.setStep(4000, 3000)
		frame.moveTo(0, -2500)
		
		var me = this
		frame.onmove = function (show, hide)
		{
			for (var i = 0; i < show.length; i++)
			{
				var box = show[i]
				if (!box.loaded)
				{
					var node = box.node,
						row = node['data-row']
					
					node.appendChild(me.renderCocktail(row.cocktail, row.ingredients))
					node.removeClassName('lazy')
					
					box.loaded = true
				}
			}
		}
		
		frame.setBoxes(boxes)
	},
	
	groupNameClicked: function (num)
	{
		this.controller.groupNameClicked(num)
	},
	
	renderGroups: function (groups)
	{
		var main = this.nodes.main
		main.empty()
		
		var items = []
		for (var i = 0, il = groups.length; i < il; i++)
		{
			var group = groups[i]
			
			var list = Nc('dl', 'group')
			main.appendChild(list)
			
			var head
			
			var name = group.name
			if (name)
				head = name
			else
			{
				var date = group.date
				if (date)
					head = months[date.getMonth()] + ' ' + date.getFullYear()
			}
			
			if (head)
			{
				var nameNode = list.appendChild(Nct('dt', 'group-name', head))
				nameNode.appendChild(Nct('span', 'count', '(' + group.rows.length + ')'))
				nameNode.addEventListener('click', this.onclickGroupName, false)
				nameNode['data-group-num'] = i
			}
			
			if (group.collapsed)
			{
				list.addClassName('collapsed')
				continue
			}
			else
				list.removeClassName('collapsed')
			
			var rows = group.rows
			for (var j = 0, jl = rows.length; j < jl; j++)
			{
				var row = rows[j],
					ingredients = row.ingredients
				
				var item = Nc('li', 'row lazy lines-' + ((((ingredients.length - 1) / 5) >> 0) + 1))
				items.push(item)
				item['data-row'] = row
				list.appendChild(item)
			}
		}
		
		this.setupVisibilityFrame(items)
	},
	
	getCocktailNode: function (cocktail, ingredients)
	{
		var cache = this.cache.cocktails,
			name = cocktail.name
			
		var node = cache[name] || (cache[name] = this.renderCocktail(cocktail, ingredients))
		return node.cloneNode(true)
	},
	
	renderCocktail: function (cocktail, ingredients)
	{
		var root = N('dl')
		
		var head = root.appendChild(Nc('dt', 'head'))
		head.appendChild(cocktail.getPreviewNode(false, true))
		head.appendChild(Nct('span', 'operator', '='))
		
		var body = root.appendChild(Nc('dd', 'body'))
		
		var inodes = []
		for (var i = 0, il = ingredients.length; i < il; i++)
			inodes[i] = ingredients[i].getPreviewNode()
		
		body.appendChild(joinWithNodeToFragment(inodes, Nct('span', 'operator', '+')))
		
		return root
	}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.Controller

var myProto =
{
	initialize: function () {},
	
	groupNameClicked: function (num) { this.model.toggleGroupCollapsedility(num) }
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.Model

var myProto =
{
	initialize: function () {},
	
	setCocktails: function (groups)
	{
		this.rawGroups = groups
		
		var res = []
		
		for (var i = 0, il = groups.length; i < il; i++)
		{
			var group = groups[i]
			
			var cocktails = group.cocktails
			
			var rows = []
			for (var j = 0, jl = cocktails.length; j < jl; j++)
			{
				var cocktail = cocktails[j]
			
				var row = rows[j] = {}
				row.cocktail = cocktail
			
				var ingredients = row.ingredients = []
			
				var recipe = cocktail.ingredients
				for (var k = 0, kl = recipe.length; k < kl; k++)
					ingredients[k] = Ingredient.getByName(recipe[k][0])
			}
			
			res[i] =
			{
				name: group.name,
				date: group.date,
				rows: rows
			}
		}
		
		this.groups = res
		this.view.renderGroups(res)
	},
	
	toggleGroupCollapsedility: function (num)
	{
		var groups = this.groups
		
		var group = groups[num]
		if (!group)
			return
		
		group.collapsed = !group.collapsed
		
		this.view.renderGroups(groups)
	}
}

Object.extend(Me.prototype, myProto)

})();


})();

;(function(){

var Papa

;(function(){

var myName = 'IngredientsList',
	Me = Papa = self[myName] = MVC.create(myName)

var myProto =
{
	initialize: function ()
	{
		this.model.initialize()
		this.view.initialize()
		this.controller.initialize()
	},
	
	bind: function (nodes)
	{
		this.view.bind(nodes)
		
		return this
	},
	
	setIngredients: function (ingredients)
	{
		this.model.setIngredients(ingredients)
	},
	
	wake: function ()
	{
		this.view.wake()
	},
	
	sleep: function ()
	{
		this.view.sleep()
	}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.View

eval(NodesShortcut.include())

var myProto =
{
	initialize: function ()
	{
		this.nodes = {}
		this.ingredientCache = {}
	},
	
	bind: function (nodes)
	{
		this.nodes = nodes
		
		var me = this
		
		var t = new Throttler(function () { me.onscroll() }, 100, 500)
		this.onscrollListener = function () { t.call() }
		
		this.wake()
	},
	
	wake: function ()
	{
		window.addEventListener('scroll', this.onscrollListener, false)
	},
	
	sleep: function ()
	{
		window.removeEventListener('scroll', this.onscrollListener, false)
	},
	
	onscroll: function ()
	{
		var frame = this.frame
		if (frame)
			frame.moveTo(window.pageXOffset, window.pageYOffset - 2500)
	},
	
	renderIngredients: function (groups)
	{
		var main = this.nodes.main
		main.empty()
		
		this.itemCache = []
		
		// console.time('render')
		for (var i = 0; i < groups.length; i++)
		{
			var group = this.renderGroup(groups[i])
			main.appendChild(group)
		}
		// console.timeEnd('render')
		
		// console.time('setupVisibilityFrame')
		this.setupVisibilityFrame(this.itemCache)
		// console.timeEnd('setupVisibilityFrame')
	},
	
	setupVisibilityFrame: function (nodes)
	{
		if (!nodes.length)
			return
		
		var boxes = Boxer.sameNodesToBoxes(nodes)
		
		var frame = this.frame = new VisibilityFrame()
		frame.setFrame(4000, 5000) // hardcoded for now
		frame.setStep(4000, 3000)
		frame.moveTo(0, -2500)
		
		frame.onmove = function (show, hide)
		{
			for (var i = 0; i < show.length; i++)
			{
				var box = show[i]
				if (!box.loaded)
				{
					box.node.ingredientNode.unLazy()
					box.loaded = true
				}
			}
		}
		
		frame.setBoxes(boxes)
	},
	
	
	renderGroup: function (group)
	{
		var root = Nc('dl', 'group')
		
		if ('name' in group)
			root.appendChild(Nct('dt', 'head', group.name))
		
		var body = Nc('dt', 'body')
		root.appendChild(body)
		
		var list = Nc('ul', 'list')
		var ingreds = group.list, itemCache = this.itemCache
		for (var i = 0; i < ingreds.length; i++)
		{
			var item = Nc('li', 'item lazy')
			itemCache.push(item)
			var ingredientNode = this.getIngredientNode(ingreds[i])
			item.appendChild(ingredientNode)
			item.ingredientNode = ingredientNode
			list.appendChild(item)
		}
		body.appendChild(list)
		
		return root
	},
	
	getIngredientNode: function (ingredient)
	{
		if ((node = this.ingredientCache[ingredient.name]))
			return node
		
		return this.ingredientCache[ingredient.name] = ingredient.getPreviewNodeLazy()
	}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.Controller

var myProto =
{
	initialize: function () {}
}

Object.extend(Me.prototype, myProto)

})();


;(function(){

var Me = Papa.Model

var myProto =
{
	initialize: function ()
	{
		this.groups = []
	},
	
	setIngredients: function (groups)
	{
		this.groups = groups
		this.view.renderIngredients(groups)
	}
}

Object.extend(Me.prototype, myProto)

})();


})();

;(function(){

var myName = 'Popup'

function Me ()
{
	this.nodes = {}
	this.listeners = {}
	this.constructor = Me
}

Me.prototype =
{
	visible: false,
	
	onhide: function () { this.hide() },
	
	bind: function (nodes)
	{
		this.nodes = nodes
		
		var me = this
		this.listeners.click = function (e) { me.onhide() }
		this.listeners.key = function (e) { e.keyCode == 27 && me.hide() }
		
		nodes.window.addEventListener('click', function (e) { e.stopPropagation() }, false)
		
		return this
	},
	
	hide: function ()
	{
		if (!this.visible)
			return
		
		this.nodes.root.hide()
		this.visible = false
		
		var me = this
		setTimeout(function () { me.unbindListeners() }, 0)
	},
	
	show: function ()
	{
		if (this.visible)
			return
		
		var nodes = this.nodes
		nodes.root.show()
		nodes.front.style.top = window.pageYOffset + 'px'
		this.visible = true
		
		var me = this
		setTimeout(function () { me.bindListeners() }, 0)
	},
	
	bindListeners: function ()
	{
		document.addEventListener('click', this.listeners.click, false)
		document.addEventListener('keydown', this.listeners.key, false)
	},
	
	unbindListeners: function ()
	{
		document.removeEventListener('click', this.listeners.click, false)
		document.removeEventListener('keydown', this.listeners.key, false)
	}
}

// Me.mixIn(EventDriven)
Me.className = myName
self[myName] = Me

})();
;(function(){

var myName = 'IngredientPopup'

eval(NodesShortcut.include())

var Super = Popup,
	superProto = Super.prototype

function Me ()
{
	Super.apply(this)
}

Me.prototype = new Super()

var myProto =
{
	setIngredient: function (ingredient)
	{
		this.ingredient = ingredient
		this.render()
	},
	
	show: function ()
	{
		Statistics.ingredientPopupOpened(this.ingredient)
		return superProto.show.apply(this, arguments)
	},
	
	render: function ()
	{
		var ingredient = this.ingredient
		
		var clone = this.cloner.create()
		this.popupRoot.appendChild(clone.root)
		
		var nodes = clone.nodes
		nodes.root = clone.root
		
		// implies this.nodes = nodes
		this.bind(nodes)
		
		var brand = ingredient.brand
		if (brand)
		{
			nodes.mark.appendChild(T(ingredient.brand))
			nodes.ingredientWindow.addClassName('branded')
			nodes.brand.appendChild(T(ingredient.mark))
			nodes.brand.href = Ingredient.ingredientsLinkByMark(ingredient.mark)
		}
		
		nodes.name.appendChild(T(ingredient.name))
		
		this.renderAllCocktailsLink(ingredient)
		this.renderSupplements(ingredient)
		
		nodes.text.innerHTML = ingredient.about
		
		nodes.image.src = ingredient.getMainImageSrc()
		
		var me = this
		setTimeout(function () { me.renderCocktails(nodes, ingredient.cocktails) }, 0)
		require('Good', function () { me.renderWhereToBuy(nodes, ingredient) })
	},
	
	renderAllCocktailsLink: function (ingredient)
	{
		var nodes = this.nodes
		
		var count = ingredient.cocktails.length
		if (count == 0)
		{
			nodes.allCocktails.hide()
			return
		}
		
		nodes.allCocktails.show()
		
		var link = nodes.allCocktailsLink
		link.href = ingredient.combinatorLink()
		
		link.appendChild(T(' ' + count + ' ' + count.plural('коктейль', 'коктейля', 'коктейлей')))
		if (ingredient.decls)
			link.appendChild(T(' ' + ingredient.decls.t))
	},
	
	renderSupplements: function (ingredient)
	{
		var nodes = this.nodes
		
		if (ingredient.cocktails.length < 6)
		{
			nodes.combinations.hide()
			return
		}
		
		var coefficients = Ingredient.defaultSupplementCoefficients()
		var supplements = Cocktail.getSupplementNamesByIngredientName(ingredient.name, coefficients)
		
		var list = nodes.combinationsList
		for (var i = 0, il = supplements.length; i < il && i < 5; i++)
		{
			var supplement = supplements[i]
			
			var item = Nc('li', 'combination')
			list.appendChild(item)
			
			var query = ingredient.name + ' + ' + supplement
			var a = Nct('a', 'link', query)
			a.href = '/combinator.html#q=' + encodeURIComponent(query)
			item.appendChild(a)
		}
	},
	
	renderWhereToBuy: function (nodes, ingredient)
	{
		var good = Good.getBySellName(ingredient.name)[0]
		if (good)
		{
			nodes.ingredientWindow.addClassName('can-buy')
			nodes.buy.appendChild(T(good.name))
			nodes.buy.href = good.getHref()
		}
	},
	
	renderCocktails: function (nodes, cocktails)
	{
		cocktails = cocktails.slice().randomize()
		
		var cl = new CocktailList()
		var nodes =
		{
			root: nodes.cocktails,
			viewport: nodes.cocktailsViewport,
			surface: nodes.cocktailsSurface,
			prev: nodes.cocktailsPrev,
			next: nodes.cocktailsNext
		}
		cl.bind(nodes)
		cl.configure({pageLength: 5, pageVelocity: 38})
		cl.setCocktails(cocktails)
	}
}

Object.extend(Me.prototype, myProto)

var myStatic =
{
	setup: function (nodes)
	{
		this.cache = {}
		
		var proto = this.prototype
		
		proto.popupRoot = nodes.root
		
		var cloner = proto.cloner = new Cloner()
		cloner.bind(nodes.popupMain, nodes.popupParts)
	},
	
	show: function (ingredient)
	{
		this.hide()
		
		var popup = this.cache[ingredient.name]
		if (!popup)
		{
			popup = this.cache[ingredient.name] = new this()
			popup.setIngredient(ingredient)
		}
		
		popup.show()
		this.popup = popup
		return popup
	},
	
	hide: function ()
	{
		var popup = this.popup
		if (!popup)
			return
		
		popup.hide()
		this.popup = null
	}
}

Object.extend(Me, myStatic)

Me.className = myName
self[myName] = Me

})();
;(function(){

var myName = 'Throttler'

function Me (callback, delay, timeout, invocant)
{
	this.callback = callback
	this.delay = delay
	this.timeout = timeout
	this.invocant = invocant || self
	this.delayTimer = 0
	this.timeoutTimer = 0
	
	var me = this
	this.timeoutCallback = function () { me.fire() }
	this.timerCallback = function () { me.fire() }
}

Me.prototype =
{
	call: function ()
	{
		this.args = arguments
		
		clearTimeout(this.delayTimer)
		this.delayTimer = setTimeout(this.timerCallback, this.delay)
		
		if (!this.timeoutTimer)
			this.timeoutTimer = setTimeout(this.timeoutCallback, this.timeout)
	},
	
	fire: function ()
	{
		var delayTimer = this.delayTimer
		if (delayTimer)
		{
			clearTimeout(delayTimer)
			this.delayTimer = 0
		}
		
		var timeoutTimer = this.timeoutTimer
		if (timeoutTimer)
		{
			clearTimeout(timeoutTimer)
			this.timeoutTimer = 0
		}
		
		this.callback.apply(this.invocant, this.args)
	}
}

Me.className = myName
self[myName] = Me

})();

;(function(){

var myName = 'QueryParser'

function Me () {}

Me.parse = function (string)
{
	var tokenizer = /([+]|^)([^+]*)/g,
		beforeRex = /^\s*/g,
		afterRex = /\s*$/g
	
	if (string === '')
	{
		var empty =
		{
			num: 0,
			begin: 0,
			op: '',
			before: '',
			value: '',
			after: '',
			end: 0
		}
		return [empty]
	}
	
	var m, tokens = [], steps = 0, lastBegin = -1
	while ((m = tokenizer.exec(string)))
	{
		if (steps++ > 50)
			break
		
		var op = m[1], body = m[2]
		
		var end = tokenizer.lastIndex, begin = end - body.length
		
		if (begin <= lastBegin)
			break
		lastBegin = begin
		
		beforeRex.lastIndex = 0
		m = beforeRex.exec(body)
		var before = m ? m[0] : ''
		
		afterRex.lastIndex = beforeRex.lastIndex
		m = afterRex.exec(body)
		var after = m ? m[0] : ''
		
		var value = body.substring(before.length, body.length - after.length)
		
		var token =
		{
			num: tokens.length,
			begin: begin,
			op: op,
			before: before,
			value: value,
			after: after,
			end: end
		}
		
		tokens.push(token)
	}
	
	return tokens
}

Me.stringify = function (tokens)
{
	var string = ''
	
	for (var i = 0, il = tokens.length; i < il; i++)
	{
		var t = tokens[i]
		string += t.op + t.before + t.value + t.after
	}
	
	return string
}

Me.getParts = function (tokens, opts)
{
	if (!opts)
		opts = {}
	
	var active = opts.exceptActive ? tokens.active : null,
		add = [], remove = []
	for (var i = 0, il = tokens.length; i < il; i++)
	{
		var t = tokens[i]
		
		if (t == active)
			continue
		
		if (!t.value)
			continue
		
		var op = t.op
		if (op == '+' || op == '')
			add.push(t.value)
		else if (op == '-')
			remove.push(t.value)
	}
	
	return {add: add, remove: remove}
}

Me.debug = function (tokens)
{
	var string = ''
	
	for (var i = 0, il = tokens.length; i < il; i++)
	{
		var t = tokens[i]
		string += t.op + '[' + t.begin + ']' + t.before + '{' + t.value + '}' + t.after + '[' + t.end + ']'
	}
	
	return string
}

Me.className = myName
self[myName] = Me

})();
;(function(){

var myName = 'Tokenizer'

function Me (input)
{
	this.input = input
}

Me.prototype =
{
	setCurrentValue: function (value)
	{
		var tokens = this.getTokens(),
			input = this.input
		
		// make the value a string
		// to be able to ask it for a length
		value = '' + value
		
		tokens.active.value = value
		input.value = QueryParser.stringify(tokens)
		input.selectionStart = input.selectionEnd = tokens.active.begin + tokens.active.before.length + value.length
	},
	
	getCurrentValue: function ()
	{
		return this.getTokens().active.value
	},
	
	getCurrentNum: function ()
	{
		return this.getTokens().active.num
	},
	
	getTokens: function ()
	{
		var input = this.input,
			value = input.value,
			cursor = input.selectionEnd
		
		var tokens = this.tokens = QueryParser.parse(value)
		
		var active = 0
		for (var i = 0, il = tokens.length; i < il; i++)
		{
			var t = tokens[i]
			
			if (t.begin <= cursor && cursor <= t.end)
				active = i
		}
		
		tokens.active = tokens[active]
		
		return tokens
	},
	
	getParts: function (tokens, exceptActive)
	{
		return QueryParser.getParts(this.getTokens(), {exceptActive: exceptActive})
	}
}

Me.className = myName
self[myName] = Me

})();

;(function(){

Array.prototype.hashValues = function (hash)
{
	if (!hash)
		hash = {}
	for (var i = 0, il = this.length; i < il; i++)
		hash[this[i]] = true
	return hash
}

Array.prototype.flatten = function ()
{
	var res = [], push = this.push
	for (var i = 0, il = this.length; i < il; i++)
	{
		var item = this[i]
		if (item.constructor == Array)
			push.apply(res, item.flatten())
		else
			res.push(item)
	}
	return res
}

var Papa = CombinatorPage, Me = Papa.Model

function DefaultState (state)
{
	for (var k in state)
	{
		var v = state[k]
		if (v !== undefined)
			this[k] = v
	}
}
DefaultState.prototype =
{
	sortBy: 'increasing-complexity'
}
Me.DefaultState = DefaultState

var myProto =
{
	sortByNames:
	[
		'от простых к сложным',
		'по дате размещения',
		'по группам',
		'по методу приготовления',
		'по алфавиту'
		// 'по количеству ингредиента'
	],
	
	sortTypeByNum:
	[
		'increasing-complexity',
		'by-date',
		'by-group',
		'by-method',
		'alphabetically'
		// 'by-strength'
	],
	
	initialize: function ()
	{
		this.ds = {}
		this.searchCache = {}
		
		this.state = new DefaultState()
	},
	
	bind: function ()
	{
		var ingredients = Ingredient.getAllNames(),
			secondNames = Ingredient.getAllSecondNames(),
			secondNamesHash = Ingredient.getNameBySecondNameHash(),
			ingredientsTags = Ingredient.getTags(),
			cocktailsTags = Cocktail.getTags()
		
		var set = ingredients.slice()
		set.push.apply(set, secondNames)
		set.push.apply(set, ingredientsTags)
		set.push.apply(set, cocktailsTags)
		set.sort()
		
		var searcher = this.searcher = new IngredientsSearcher(set, secondNamesHash)
		this.view.setCompleterDataSource(searcher)
		
		this.view.renderSortbyOptions(this.sortByNames)
		this.view.renderSortby(this.sortTypeByNum.indexOf(this.state.sortBy))
		
		var favorites = searcher.favorites = {}
		
		var ingredientsTagsHash = this.ingredientsTagsHash = {}
		for (var i = 0, il = ingredientsTags.length; i < il; i++)
		{
			var tag = ingredientsTags[i]
			ingredientsTagsHash[tag.toLowerCase()] = tag
			favorites[tag] = true
		}
		
		var cocktailsTagsHash = this.cocktailsTagsHash = {}
		for (var i = 0, il = cocktailsTags.length; i < il; i++)
		{
			var tag = cocktailsTags[i]
			cocktailsTagsHash[tag.toLowerCase()] = tag
			favorites[tag] = true
		}
	},
	
	updateData: function ()
	{
		var state = this.state,
			add = state.add,
			remove = state.remove
		
		if (!add.length && !remove.length)
		{
			this.view.renderCocktails(null)
			return
		}
		
		var cocktails = this.getCocktailsByQuery(add, remove)
		
		if (cocktails.length == 0)
		{
			this.view.renderCocktails(cocktails, 0)
			this.suggestSome(add, remove)
			return
		}
		
		var sorted
		switch (state.sortBy)
		{
			case 'increasing-complexity':
				sorted = this.sortByIncreasingComplexity(cocktails)
			break
			
			case 'alphabetically':
				sorted = this.sortAlphabetically(cocktails)
			break
			
			case 'by-group':
				sorted = this.sortByGroup(cocktails)
			break
			
			case 'by-method':
				sorted = this.sortByMethod(cocktails)
			break
			
			case 'by-date':
				sorted = this.sortByDate(cocktails)
			break
			
			case 'by-strength':
				sorted = this.sortByStrength(cocktails, add)
			break
		}
		
		var stats = this.calculateStats(cocktails)
		
		// oowf, need to update the view
		this.view.renderCocktails(sorted, cocktails.length, stats)
	},
	
	sortByIncreasingComplexity: function (cocktails)
	{
		cocktails = cocktails.slice()
		cocktails.sort(function (a, b) { return a.ingredients.length - b.ingredients.length || a.tools.length - b.tools.length || a.name.localeCompare(b.name) })
		return [{cocktails: cocktails}]
	},
	
	sortAlphabetically: function (cocktails)
	{
		cocktails.sort(function (a, b) { return a.name.localeCompare(b.name) })
		
		var letter, group, res = []
		for (var i = 0, il = cocktails.length; i < il; i++)
		{
			var cocktail = cocktails[i]
			
			var l = cocktail.name.charAt(0)
			
			if (l == letter)
			{
				group.push(cocktail)
				continue
			}
			
			letter = l
			group = [cocktail]
			res.push({cocktails: group, name: letter})
		}
		
		return res
	},
	
	sortByGroup: function (cocktails)
	{
		cocktails.sort(function (a, b) { return a.ingredients.length - b.ingredients.length })
		
		var byGroup = {}
		
		for (var i = 0, il = cocktails.length; i < il; i++)
		{
			var cocktail = cocktails[i]
			
			var groups = cocktail.groups
			for (var j = 0, jl = groups.length; j < jl; j++)
			{
				var group = groups[j]
				
				var arr = byGroup[group]
				if (arr)
					arr.push(cocktail)
				else
					byGroup[group] = [cocktail]
			}
		}
		
		var groups = Cocktail.getGroups(),
			sorted = []
		for (var i = 0, il = groups.length; i < il; i++)
		{
			var group = groups[i]
			if (byGroup[group])
				sorted.push(group)
		}
		
		var groups = []
		for (var i = 0, il = sorted.length; i < il; i++)
		{
			var group = sorted[i]
			groups.push({name: group, cocktails: byGroup[group]})
		}
		
		return groups
	},
	
	sortByMethod: function (cocktails)
	{
		cocktails.sort(function (a, b) { return a.ingredients.length - b.ingredients.length })
		
		var byMethod = {}
		
		for (var i = 0, il = cocktails.length; i < il; i++)
		{
			var cocktail = cocktails[i],
				method = cocktail.method
			
			var arr = byMethod[method]
			if (arr)
				arr.push(cocktail)
			else
				byMethod[method] = [cocktail]
		}
		
		var methods = Cocktail.getMethods(),
			sorted = []
		for (var i = 0, il = methods.length; i < il; i++)
		{
			var method = methods[i]
			if (byMethod[method])
				sorted.push(method)
		}
		
		var groups = []
		for (var i = 0, il = sorted.length; i < il; i++)
		{
			var method = sorted[i]
			groups.push({name: method, cocktails: byMethod[method]})
		}
		
		return groups
	},
	
	sortByDate: function (cocktails)
	{
		cocktails.sort(function (a, b) { return b.added - a.added })
		
		var groups = []
		
		var currentKey = 0, month = null
		for (var i = 0, il = cocktails.length; i < il; i++)
		{
			var cocktail = cocktails[i]
			
			var added = new Date(cocktail.added * 1000)
			
			var key = added.getFullYear() * 100 + added.getMonth()
			if (key == currentKey)
			{
				month.push(cocktail)
				continue
			}
			
			currentKey = key
			month = [cocktail]
			groups.push({date: added, cocktails: month})
		}
		
		return groups
	},
	
	sortByStrength: function (cocktails, add)
	{
		var kByIngredient = {}
		for (var i = 0, il = add.length; i < il; i++)
			kByIngredient[add[i]] = 1 / (i * 1000 + 1)
		
		var weightByName = {}
		for (var i = 0, il = cocktails.length; i < il; i++)
		{
			var cocktail = cocktails[i]
			
			var weight = 0// , total = 0
			
			var parts = cocktail.ingredients
			for (var j = 0, jl = parts.length; j < jl; j++)
			{
				var part = parts[j]
				
				var volume = parseFloat(part[1])
				// total += volume
				
				var k = kByIngredient[part[0]]
				if (!k)
					continue
				
				weight += k * parseFloat(part[1])
			}
			
			weightByName[cocktail.name] = weight // / total
		}
		
		cocktails.sort(function (a, b) { return weightByName[b.name] - weightByName[a.name] })
		
		return [{cocktails: cocktails}]
	},
	
	calculateStats: function (cocktails)
	{
		var rating = {}
		for (var i = 0, il = cocktails.length; i < il; i++)
		{
			var parts = cocktails[i].ingredients
			for (var j = 0, jl = parts.length; j < jl; j++)
			{
				var name = parts[j][0]
				var count = rating[name]
				if (count)
					rating[name]++
				else
					rating[name] = 1
			}
		}
		
		var all = Object.keys(rating)
		all.sort(function (a, b) { return rating[b] - rating[a] })
		all = all.slice(0, 7)
		
		var top = []
		for (var i = 0, il = all.length; i < il; i++)
		{
			var name = all[i]
			
			top[i] =
			{
				ingredient: Ingredient.getByName(name),
				rating: rating[name]
			}
		}
		
		return {top: top}
	},
	
	combine: function (arr)
	{
		var res = [], seen = {}, total = 0
		
		res.push(arr.slice())
		if (arr.length == 1)
			return res
		
		function walk (a)
		{
			total++
			
			for (var i = a.length - 1; i >= 0; i--)
			{
				
				var v = a.slice()
				v.splice(i, 1)
				
				var s = ''+v
				if (seen[s])
					continue
				seen[s] = true
				
				res.push(v)
				
				if (v.length == 1)
					continue
				
				walk(v)
			}
		}
		
		walk(arr)
		
		return res
	},
	
	suggestSome: function (add, remove)
	{
		var suggestions = []
		
		var combinatios = this.combine(add)
		combinatios.sort(function (a, b) { return b.length - a.length })
		
		var begin = +new Date()
		for (var i = 0, il = combinatios.length; i < il; i++)
		{
			var query = combinatios[i]
			
			var set = this.getCocktailsByQuery(query, [])
			if (set.length)
				suggestions.push({add: this.collapseQueryObjects(query), count: set.length})
			
			if (i % 25 == 0 && new Date() - begin > 250)
				break
		}
		
		this.view.renderSuggestions(suggestions)
	},
	
	setState: function (newState)
	{
		this.statView(newState.add, newState.remove)
		
		var add = this.expandQueryNames(newState.add)
		var remove = this.expandQueryNames(newState.remove)
		
		this.setDuplicates(add, remove)
		
		var state = this.state = new DefaultState(newState)
		state.add = add
		state.remove = remove
		
		this.updateData()
		
		this.view.renderQuery(state.query)
		this.lastQuery = state.query
		
		if (state.sortBy)
			this.view.renderSortby(this.sortTypeByNum.indexOf(state.sortBy))
		
		this.view.scrollTo(+state.scrollTop || 0)
		
		if (state.ingredientPopup)
		{
			var ingredient = Ingredient.getByName(state.ingredientPopup)
			this.view.showIngredient(ingredient)
		}
		else
			this.view.showIngredient(null)
	},
	
	setQuery: function (add, remove, query)
	{
		if (query === this.lastQuery)
			return
		this.lastQuery = query
		
		this.statSearch(query)
		this.statView(add, remove)
		
		add = this.expandQueryNames(add)
		remove = this.expandQueryNames(remove)
		
		this.setDuplicates(add, remove)
		
		var state = this.state
		state.add = add
		state.remove = remove
		state.query = query
		
		state.scrollTop = 0
		this.view.scrollTo(state.scrollTop)
		
		this.updateData()
		
		this.view.setBookmark(state)
	},
	
	queryChanged: function (add, remove)
	{
		add = this.expandQueryNames(add)
		remove = this.expandQueryNames(remove)
		
		this.setDuplicates(add, remove)
	},
	
	setDuplicates: function (add, remove)
	{
		var duplicates = this.searcher.duplicates = {}
		this.hashDuplicates(add, duplicates)
		this.hashDuplicates(remove, duplicates)
	},
	
	hashDuplicates: function (arr, hash)
	{
		for (var i = 0, il = arr.length; i < il; i++)
		{
			var item = arr[i],
				type = item.type
			
			hash[item] = true
			
			if (type == 'ingredient-tag')
			{
				var tag = item.valueOf(),
					names = item.names
				for (var j = 0, jl = names.length; j < jl; j++)
					hash[names[j]] = tag
				continue
			}
		}
	},
	
	setSortBy: function (typeNum)
	{
		var state = this.state
		
		state.sortBy = this.sortTypeByNum[typeNum]
		
		this.view.setBookmark(state)
		this.updateData()
	},
	
	
	getCocktailsByQuery: function (add, remove)
	{
		var key = add.join(':') + '::' + remove.join(':')
		
		// look up the cache
		var cocktails = this.searchCache[key]
		if (!cocktails)
			cocktails = this.searchCache[key] = this.searchCocktails(add, remove)
		
		return cocktails
	},
	
	searchCocktails: function (add, remove)
	{
		var cocktails = Cocktail.getAll()
		
		for (var i = 0, il = add.length; i < il; i++)
		{
			var item = add[i],
				type = item.type
			
			if (type == 'ingredient')
			{
				cocktails = Cocktail.getByIngredientNames([item.valueOf()], {db: cocktails})
				continue
			}
			
			if (type == 'ingredient-tag')
			{
				cocktails = Cocktail.getByIngredientNames(item.names, {db: cocktails, count: 1})
				continue
			}
			
			if (type == 'cocktail-tag')
			{
				cocktails = Cocktail.getByTags([item], {db: cocktails, count: 1})
				continue
			}
		}
		
		// yes, needs refactoring
		for (var i = 0, il = remove.length; i < il; i++)
		{
			var item = remove[i],
				type = item.type
			
			if (type == 'ingredient')
			{
				cocktails = Cocktail.getByIngredientNames([item.valueOf()], {db: cocktails}).rest
				continue
			}
			
			if (type == 'ingredient-tag')
			{
				cocktails = Cocktail.getByIngredientNames(item.names, {db: cocktails, count: 1}).rest
				continue
			}
			
			if (type == 'cocktail-tag')
			{
				cocktails = Cocktail.getByTags([item], {db: cocktails, count: 1}).rest
				continue
			}
		}
		
		return cocktails
	},
	
	collapseQueryObjects: function (arr)
	{
		var names = []
		
		for (var i = 0, il = arr.length; i < il; i++)
			names[i] = arr[i].valueOf()
		
		return names
	},
	
	expandQueryNames: function (arr)
	{
		var ingredientsTagsHash = this.ingredientsTagsHash,
			cocktailsTagsHash = this.cocktailsTagsHash
		
		var res = [], seen = {}
		for (var i = 0; i < arr.length; i++)
		{
			var item = arr[i]
			
			if (seen[item])
				continue
			seen[item] = true
			
			var tag = ingredientsTagsHash[item.toLowerCase()]
			if (tag)
			{
				var name = new String(tag)
				name.type = 'ingredient-tag'
				var names = name.names = []
				
				var group = Ingredient.getByTag(tag)
				for (var j = 0, jl = group.length; j < jl; j++)
					names[j] = group[j].name
				
				res.push(name)
				continue
			}
			
			var tag = cocktailsTagsHash[item.toLowerCase()]
			if (tag)
			{
				var name = new String(tag)
				name.type = 'cocktail-tag'
				res.push(name)
				continue
			}
			
			var ingredient = Ingredient.getByNameCI(item)
			if (ingredient)
			{
				var name = new String(ingredient.name)
				name.type = 'ingredient'
				res.push(name)
				continue
			}
		}
		return res
	},
	
	updateAllIngredients: function ()
	{
		var ingredients = this.allIngredients
		if (ingredients)
			return
		
		Ingredient.calculateEachIngredientUsage()
		ingredients = this.allIngredients = Ingredient.getAll()
		
		var groups = this.groupByGroup(ingredients)
		this.sortGoupsBy(groups, this.sortByUsage)
		
		this.view.renderInitialBlock(groups)
	},
	
	groupByGroup: function (all)
	{
		var groups = Ingredient.getGroups()
		
		var data = []
		{
			var slices = {}
			for (var i = 0; i < groups.length; i++)
			{
				var list = [], name = groups[i]
				slices[name] = list
				data.push({name: name, list: list})
			}
			
			for (var i = 0; i < all.length; i++)
			{
				var ingred = all[i]
				slices[ingred.group].push(ingred)
			}
		}
		return data
	},
	
	sortGoupsBy: function (data, func)
	{
		for (var i = 0; i < data.length; i++)
			data[i].list.sort(func)
	},
	
	sortByUsage: function (a, b) { return b.cocktails.length - a.cocktails.length },
	
	updateExamples: function ()
	{
		var examples = this.guessExamples() || [['водка'], ['водка', 'сок']]
		this.view.renderExamples(examples)
	},
	
	guessExamples: function ()
	{
		var base = this.chooseExampleIngredient(),
			baseName = base.name
		
		var coefficients = Ingredient.defaultSupplementCoefficients()
		var supplements = Ingredient.getByNames(Cocktail.getSupplementNamesByIngredientName(baseName, coefficients))
		if (supplements.length == 0)
			return
		
		var names = []
		
		// collect “main” tags from all ingredients
		for (var i = 0, il = supplements.length; i < il; i++)
		{
			var supplement = supplements[i]
			
			// names.push(supplement.name)
			
			var tags = supplement.tags
			if (tags.length >= 2)
				names.push(tags[0])
		}
		
		var second = names.random(1)[0]
		return [[baseName], [baseName, second]]
	},
	
	chooseExampleIngredient: function ()
	{
		var ingredients = Ingredient.getByTag('Сочетайзер')
		if (ingredients.length)
			return ingredients.random(1)[0]
		
		var ingredients = Ingredient.getByGroup('Крепкий алкоголь')
		Ingredient.calculateEachIngredientUsage()
		ingredients.sort(this.sortByUsage)
		return ingredients.slice(0, 8).random(1)[0]
	},
	
	selectIngredient: function (ingredient)
	{
		var state = this.state
		
		if (ingredient)
		{
			state.ingredientPopup = ingredient.name
			this.view.showIngredient(ingredient)
		}
		else
		{
			delete state.ingredientPopup
			this.view.showIngredient(null)
		}
		
		this.view.setBookmark(state)
	},
	
	setScrollTop: function (v)
	{
		this.state.scrollTop = v
	},
	
	statSearch: function (query)
	{
		if (/\S/.test(query))
			Statistics.combinatorQueryRaw(query)
	},
	
	statView: function (add, remove)
	{
		if (!add.length && !remove.length)
			return
		
		var lowerAdd = []
		for (var i = 0, il = add.length; i < il; i++)
			lowerAdd[i] = add[i].toLowerCase()
		lowerAdd.sort()
		
		var lowerRemove = []
		for (var i = 0, il = remove.length; i < il; i++)
			lowerRemove[i] = remove[i].toLowerCase()
		lowerRemove.sort()
		
		var query = lowerAdd.join(' + ')
		if (lowerRemove.length)
			query += ' - ' + lowerRemove.join(' - ')
		
		Statistics.combinatorQueryViewed(query)
	}
}

Object.extend(Me.prototype, myProto)

})();
;(function(){

var Papa = CombinatorPage, Me = Papa.View

eval(NodesShortcut.include())

var UrlEncodeLight = {}
Object.extend(UrlEncodeLight, UrlEncode)
UrlEncodeLight.encode = function (v) { return ('' + v).replace('&', '%26') }

var myProto =
{
	initialize: function ()
	{
		this.nodes = {}
	},
	
	bind: function (nodes)
	{
		this.nodes = nodes
		
		var inco = this.inco = new IngredientedCocktailList()
		inco.bind({main: nodes.cocktailList})
		
		var inli = this.inli = new IngredientsList()
		inli.bind({main: nodes.ingredientsList})
		
		var me = this
		
		var completer = this.completer = new AddingInputAutocompleter()
		completer.bind({main: nodes.queryInput, list: nodes.ingredientComplete})
		completer.addEventListener('accept', function (e) { me.queryAccepted(e.add, e.remove) }, false)
		completer.addEventListener('changed', function (e) { me.queryChanged(e.add, e.remove) }, false)
		nodes.queryInput.focus()
		
		nodes.searchForm.addEventListener('submit', function (e) { e.preventDefault(); setTimeout(function () { me.searchFormSubmitted() }, 50) }, false)
		
		nodes.plusButton.addEventListener('click', function (e) { me.plusButtonClicked() }, false)
		nodes.resetButton.addEventListener('click', function (e) { me.resetButtonClicked() }, false)
		
		var controller = this.controller
		
		var s = this.sortbySelect = new Selecter()
		s.bind(nodes.sortbySelect)
		s.addEventListener('select', function (e) { controller.setSortBy(+e.data.num) }, false)
		
		nodes.ingredientsList.addEventListener('click', function (e) { me.maybeIngredientClicked(e.target) }, false)
		nodes.cocktailList.addEventListener('click', function (e) { me.maybeIngredientClicked(e.target) }, false)
		
		var lh = this.locationHash = new LocationHash().bind(window)
		lh.addEventListener('change', function (e) { me.locationHashUpdated() }, false)
		
		var t = new Throttler(function () { me.controller.windowScrolled(window.pageYOffset) }, 100, 500)
		window.addEventListener('scroll', function () { t.call() }, false)
		
		return this
	},
	
	plusButtonClicked: function ()
	{
		var input = this.nodes.queryInput
		
		this.completer.reset()
		
		var value = input.value
		if (/\S/.test(value))
			input.value = value.replace(/\s*$/, ' + ')
		
		input.focus()
	},
	
	resetButtonClicked: function ()
	{
		var input = this.nodes.queryInput
		
		input.value = ''
		input.focus()
		this.controller.setQuery([], [], '')
	},
	
	locationHashUpdated: function ()
	{
		var bookmark = UrlEncodeLight.parse(this.locationHash.get())
		
		var query = bookmark.q || ''
		
		var parts = QueryParser.getParts(QueryParser.parse(query))
		
		var state =
		{
			add: parts.add,
			remove: parts.remove,
			query: query,
			sortBy: bookmark.s,
			ingredientPopup: bookmark.i,
			scrollTop: bookmark.y
		}
		
		this.controller.setState(state)
	},
	
	renderQuery: function (query)
	{
		this.completer.reset()
		this.nodes.queryInput.value = query
	},
	
	setBookmark: function (state)
	{
		var bookmark =
		{
			q: state.query,
			s: state.sortBy,
			i: state.ingredientPopup,
			y: state.scrollTop
		}
		
		for (var k in bookmark)
		{
			var v = bookmark[k]
			if (!v && v !== 0)
				delete bookmark[k]
		}
		
		this.locationHash.set(UrlEncodeLight.stringify(bookmark))
	},
	
	searchFormSubmitted: function ()
	{
		this.completer.apply(this.nodes.queryInput.value)
	},
	
	setCompleterDataSource: function (ds)
	{
		this.completer.setDataSource(ds)
	},
	
	queryAccepted: function (add, remove)
	{
		for (var i = 0, il = add.length; i < il; i++)
			add[i] = add[i].trim().replace(/\s+/g, ' ')
		
		for (var i = 0, il = remove.length; i < il; i++)
			remove[i] = remove[i].trim().replace(/\s+/g, ' ')
		
		this.controller.setQuery(add, remove, this.nodes.queryInput.value)
	},
	
	queryChanged: function (add, remove)
	{
		this.controller.queryChanged(add, remove)
	},
	
	renderSortbyOptions: function (options)
	{
		var s = this.sortbySelect
		s.setOptions(options)
		s.renderSelected(0)
	},
	
	renderSortby: function (selected)
	{
		this.sortbySelect.renderSelected(selected)
	},
	
	renderCocktails: function (cocktails, total, stats)
	{
		var nodes = this.nodes,
			output = nodes.output
		
		output.removeClassName('initial-state')
		output.removeClassName('result-state')
		output.removeClassName('empty-state')
		
		var inco = this.inco, inli = this.inli
		
		if (!cocktails)
		{
			output.addClassName('initial-state')
			inco.setCocktails([])
			inco.sleep()
			inli.wake()
			this.controller.updateInitialBlock()
			return
		}
		
		if (cocktails.length == 0)
		{
			inco.sleep()
			inli.sleep()
			output.addClassName('empty-state')
			inco.setCocktails(cocktails)
			return
		}
		
		inco.wake()
		inli.sleep()
		
		output.addClassName('result-state')
		inco.setCocktails(cocktails)
		
		nodes.totalCocktails.firstChild.nodeValue = total + ' ' + total.plural('коктейль', 'коктейля', 'коктейлей')
		nodes.sortedWord.firstChild.nodeValue = total.plural('отсортирован', 'отсортированы', 'отсортированы')
		
		var statsNode = nodes.stats
		statsNode.empty()
		
		var top = stats.top, parts = []
		for (var i = 0, il = top.length; i < il; i++)
		{
			var item = top[i]
			parts[i] = item.ingredient.name + ' (' + item.rating + ')'
		}
		
		statsNode.appendChild(T('Чаще ' + parts.length.plural('встречается', 'встречаются', 'встречаются') + ' ' + parts.join(', ') + '.'))
	},
	
	renderInitialBlock: function (groups)
	{
		this.inli.setIngredients(groups)
	},
	
	renderExamples: function (examples)
	{
		var nodes = this.nodes,
			s = nodes.hintSingle,
			d = nodes.hintDouble
		
		s.firstChild.nodeValue = examples[0][0]
		s.href = '#q=' + encodeURIComponent(examples[0])
		
		var pair = examples[1].join(' + ')
		d.firstChild.nodeValue = pair
		d.href = '#q=' + encodeURIComponent(pair)
	},
	
	renderSuggestions: function (suggestions)
	{
		var nodes = this.nodes,
			root = nodes.suggestions,
			list = nodes.suggestionsList
		
		list.empty()
		
		if (suggestions.length == 0)
		{
			root.toggleClassName('empty', true)
			return
		}
		
		root.toggleClassName('empty', false)
		
		for (var i = 0, il = suggestions.length; i < il; i++)
		{
			var s = suggestions[i]
			
			var item = Nc('li', 'item')
			
			var query = s.add.join(' + ')
			var link = Nct('a', 'link', query)
			link.href= '#q=' + encodeURIComponent(query)
			item.appendChild(link)
			
			item.appendChild(Nct('span', 'count', ' (' + s.count + ' ' + s.count.plural('коктейль', 'коктейля', 'коктейлей') + ')'))
			
			list.appendChild(item)
		}
	},
	
	findIngredientInParents: function (node)
	{
		do
		{
			var ingredient = node['data-ingredient']
			if (ingredient)
				return ingredient
		}
		while ((node = node.parentNode))
		
		return null
	},
	
	maybeIngredientClicked: function (target)
	{
		var ingredient = this.findIngredientInParents(target)
		
		if (ingredient)
			this.controller.ingredientSelected(ingredient)
	},
	
	showIngredient: function (ingredient)
	{
		if (ingredient)
		{
			var popup = IngredientPopup.show(ingredient)
			var controller = this.controller
			popup.onhide = function () { controller.ingredientSelected(null) }
		}
		else
			IngredientPopup.hide()
	},
	
	scrollTo: function (top)
	{
		window.scrollTo(0, top)
	}
}

Object.extend(Me.prototype, myProto)

})();

;(function(){

var Papa = CombinatorPage, Me = Papa.Controller

var myProto =
{
	initialize: function ()
	{
		this.state = {}
	},
	
	setState: function (state)
	{
		this.model.setState(state)
	},
	
	setQuery: function (add, remove, query)
	{
		this.model.setQuery(add, remove, query)
	},
	
	queryChanged: function (add, remove)
	{
		this.model.queryChanged(add, remove)
	},
	
	setSortBy: function (type)
	{
		this.model.setSortBy(type)
	},
	
	updateInitialBlock: function ()
	{
		this.model.updateAllIngredients()
		this.model.updateExamples()
	},
	
	ingredientSelected: function (ingredient)
	{
		this.model.selectIngredient(ingredient)
	},
	
	windowScrolled: function (v)
	{
		this.model.setScrollTop(v)
	}
}

Object.extend(Me.prototype, myProto)

})();




;(function(){

function onready ()
{
	UserAgent.setupDocumentElementClassNames()
	
	var nodes =
	{
		root: document.body,
		popupMain: $('ingredient-info-popup'),
		popupParts:
		{
			window: $$('#ingredient-info-popup .popup-window')[0],
			front: $$('#ingredient-info-popup .popup-front')[0],
			ingredientWindow: $$('#ingredient-info-popup .popup-window .ingredient-window')[0],
			image: $$('#ingredient-info-popup .description .image')[0],
			mark: $$('#ingredient-info-popup .description .about .mark')[0],
			brand: $$('#ingredient-info-popup .description .about .brand .link')[0],
			buy: $$('#ingredient-info-popup .description .about .where-to-buy .link')[0],
			name: $$('#ingredient-info-popup .description .about .name')[0],
			text: $$('#ingredient-info-popup .description .about .text')[0],
			allCocktails: $$('#ingredient-info-popup .description .about .all-cocktails')[0],
			allCocktailsLink: $$('#ingredient-info-popup .description .about .all-cocktails .link')[0],
			combinations: $$('#ingredient-info-popup .description .about .combinations')[0],
			combinationsList: $$('#ingredient-info-popup .description .about .combinations .list')[0],
			cocktails: $$('#ingredient-info-popup .cocktail-list')[0],
			cocktailsViewport: $$('#ingredient-info-popup .cocktail-list .viewport')[0],
			cocktailsSurface: $$('#ingredient-info-popup .cocktail-list .surface')[0],
			cocktailsPrev: $$('#ingredient-info-popup .cocktail-list .prev')[0],
			cocktailsNext: $$('#ingredient-info-popup .cocktail-list .next')[0]
		}
	}
	
	IngredientPopup.setup(nodes)
	
	Ingredient.calculateEachIngredientUsage()
	
	
	var nodes =
	{
		output: $('output'),
		cocktailList: $$('#output .result-block .ingrediented-cocktail-list')[0],
		stats: $$('#output .result-block .stats')[0],
		queryInput: $$('#search-box .query')[0],
		searchForm: $$('#search-box form')[0],
		plusButton: $$('#search-box .plus')[0],
		resetButton: $$('#search-box .reset')[0],
		ingredientComplete: $$('#search-box .autocomplete')[0],
		totalCocktails: $$('#output .sort-line .cocktail-count')[0],
		sortedWord: $$('#output .sort-line .sorted-word')[0],
		suggestions: $$('#output .empty-block .suggestions')[0],
		suggestionsList: $$('#output .empty-block .suggestions .list')[0],
		ingredientsList: $$('#output .initial-block .ingredients-list')[0],
		helpLine: $$('#output .help-line')[0],
		hintSingle: $$('#output .help-line .hint.single')[0],
		hintDouble: $$('#output .help-line .hint.double')[0],
		
		sortbySelect:
		{
			main: $('sortby-select'),
			button: $$('#sortby-select .button')[0],
			options: $$('#sortby-select .options')[0]
		}
	}
	
	var widget = new CombinatorPage()
	widget.bind(nodes)
}

$.onready(onready)

})();